package edu.cmich.cps.cps240.partyprincesspalace.hw06;

import java.util.Vector;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;


/**
 * A Chip object, has inputs/outputs and a name,
 * can be drawn on the workspace.
 * @author kingm1sa
 */

public class Chip implements WorkspaceObj, Serializable{
	private static final long serialVersionUID = -8897112460908212370L;
	//protected boolean selected = false;
	protected Vector<ChipPin> inputs;
	protected Vector<ChipPin> outputs;
	protected String name;
	protected Rectangle rect;
	protected int pinLength = 10;
	
	/**
	 * Constructor, which creates a chip with the properties as they are
	 * passed in, no connections are made. 
	 * @param p_rect is the rectangle that the created chip should get.
	 * @param p_inputs are the inputs that the created chip should get.
	 * @param p_outputs are the outputs that the created chip should get.
	 * @param p_name is the name that the created chip should get.
	 */
	public Chip(Rectangle p_rect, Vector<ChipPin> p_inputs, Vector<ChipPin> p_outputs, String p_name){
		if (p_rect != null){
			this.rect = new Rectangle(p_rect);
		} else{
			this.rect = new Rectangle();
		}
		
		if (p_inputs != null){
			this.inputs = p_inputs;
		} else{
			this.inputs = new Vector<ChipPin>();
		}
		
		if (p_outputs != null){
			this.outputs = p_outputs;
		} else{
			this.outputs = new Vector<ChipPin>();
		}
		this.setName(p_name);
		
		//Set inputs and outputs to point to the chip as their parent
		for (ChipPin input : inputs)
		{
			input.setParent(this);
		}
		
		for (ChipPin output : outputs)
		{
			output.setParent(this);
		}
		
		this.autoFillPinLocation();
	}
	
	/**
	 * Copy constructor, which creates a chip with the same
	 * properties as the given original chip, but without any
	 * connections. 
	 * @param original the chip to copy
	 */
	public Chip(Chip original)
	{
		super();
		if (original == null) return;
		this.inputs = new Vector<ChipPin>();
		this.outputs = new Vector<ChipPin>();
		if (original.inputs != null){
			for (int i = 0; i < original.inputs.size(); i++)
			{	
				this.inputs.add(new ChipPin(original.inputs.get(i).getName(), this, null));
			}
		}
		
		if (original.outputs != null){
			for (int i = 0; i < original.outputs.size(); i++)
			{
				this.outputs.add(new ChipPin(original.outputs.get(i).getName(), this, null));
			}
		}
		
		
		if (original.rect != null){
			this.rect = new Rectangle(original.rect);
		}else {
			this.rect = new Rectangle();
		}
		
		this.setName(original.getName());
		this.autoFillPinLocation();
	}
	
	
	/**
	 * Draws the chip to the given graphics context and
	 * draws each of the wire's lines to the given graphics context
	 * @param g the graphics context to draw the chip to.
	 * @throws NullPointerException if Graphics g (the graphical context) is null
	 */
	public void draw(Graphics g){
		Graphics2D g2d = (Graphics2D)g;
		
		g2d.draw(rect);
		//TODO: Change the offset by a part of the height and width, instead of absolute values.
		g2d.drawString(name, (int)rect.getMinX() + 5, (int)rect.getMaxY() - 5);
		
		// Draws the inputs and outputs (respectively, for each of the two loops)
		
		//draws the inputs, separated by a distance equidistant between 
		//each other and the edges of the chip.
		for (int i = 0; i < this.inputs.size(); i++){
			g2d.drawLine((int)this.inputs.get(i).getLocation().getX(), 
					(int)this.inputs.get(i).getLocation().getY(),
					(int)this.rect.getX(), 
					(int)this.inputs.get(i).getLocation().getY());
			this.inputs.get(i).draw(g);
		}
		
		//draws the outputs, also separated equidistant between each other
		for (int i = 0; i < this.outputs.size(); i++){
			g2d.drawLine((int)(this.rect.getX() + this.rect.getWidth()), 
					(int)this.outputs.get(i).getLocation().getY(),
					(int)this.outputs.get(i).getLocation().getX(), 
					(int)this.outputs.get(i).getLocation().getY());
			this.outputs.get(i).draw(g);
		}
	}
	
	/**
	 * This method iterates through all of the pins that this chip has and assigns each of them a location.
	 */
	private void autoFillPinLocation(){
		for (int i = 0; i < this.inputs.size(); i++){
			int verticalGap = (int)(rect.getY() + ((((this.rect.height/Workspace.getGridSquareSize())-inputs.size())/2)+1)*10);
			Point newLoc = new Point((int)this.rect.getX() - this.pinLength, verticalGap + i*10);
			this.inputs.get(i).setLocation(Workspace.snapToGrid(newLoc));
		}
		
		for (int i = 0; i < this.outputs.size(); i++){
			int verticalGap = (int)(rect.getY() + ((((this.rect.height/Workspace.getGridSquareSize())-outputs.size())/2)+1)*10);
			Point newLoc = new Point((int)this.rect.getX() + this.rect.width + this.pinLength, verticalGap + i*10);
			this.outputs.get(i).setLocation(Workspace.snapToGrid(newLoc));
		}
	}
	
	/**
	 * Helper function for making sure that the ChipPins are drawn smoothly during a drag operation
	 * Does NOT snap-to-grid.
	 * @param delta The distance and direction that the mouse has traveled since the last drag operation
	 * 
	 */
	private void updatePinLocation(Point delta)
	{
		for (ChipPin pin : inputs)
		{
			pin.setLocation(new Point(pin.getLocation().x - delta.x,
									pin.getLocation().y - delta.y));
		}
		
		for (ChipPin pin : outputs)
		{
			pin.setLocation(new Point(pin.getLocation().x - delta.x,
					pin.getLocation().y - delta.y));
		}
	}
	
	
	/**
	 * Moves the chip to a new location.
	 * @param p_location (Point) contains the new (x,y) coordinates of the chip.
	 */
	public void move(Point p_location){
		Point delta = new Point();
		delta.x = rect.x - p_location.x;
		delta.y = rect.y - p_location.y;
		
		this.rect.x = p_location.x;
		this.rect.y = p_location.y;
		this.updatePinLocation(delta);
	}
	
	/**
	 * @return the location of the upper left hand corner of the chip
	 */
	public Point returnLocation(){
		Point point = new Point();
		point.x = this.rect.x;
		point.y = this.rect.y;
		return point;
	}
	
	
	/**
	 * Removes the chip from the workspace by disconnecting it from all wires.
	 */
	public void remove(){
		
		for (ChipPin input : inputs)
		{
			if (input != null)
			{
				this.detachFrom(input.getPartner());
			}
		}
		
		for (ChipPin output : outputs)
		{
			if (output != null)
			{
				this.detachFrom(output.getPartner());
			}
		}
	}
	
	
	/**
	 * isClose takes a location defined by mouseClick and sees if the chip was clicked.
	 * @param mouseClick is a series of coordinates that describes where the mouse was clicked.
	 * @return the output is true if the mouseClick intersected the chip, else false.
	 */
	public boolean isClose(Point mouseClick){
		if((mouseClick.y >= this.rect.y) && (mouseClick.y <= (int)(this.rect.y + this.rect.getHeight()))){
			if((mouseClick.x >= (this.rect.x - this.pinLength)) && (mouseClick.x <=
				(int)(this.rect.x + this.rect.getWidth() + this.pinLength)))
				return true;
		}
		
		return false;
	}
	
	/*
	 * dead code
	/**
	 * Attaches a wire to an input. Maybe add another method that
	 * includes the index of the desired input.
	 * @param p_input is the WirePin to connect our input to.
	 * @return false on fail to connect.
	 */
	/*
	public boolean attachInput(WirePin p_input){
		int i = 0;
		
		while (i <= this.inputs.size()){
			if (this.inputs.get(i) == null){
				i++;
			}
			else if (this.inputs.get(i) != null){
				this.inputs.get(i).setPartner(p_input);
				this.inputs.get(i).getPartner().setPartner(this.inputs.get(i));
				return true;
			}
		}
		
		return false;
	}
	*/
	
	/*
	 * dead code
	/**
	 * Attaches a wire to an output. Maybe add another method that
	 * includes the index of the desired output.
	 * @param p_output is the WirePin to connect our output to.
	 * @return false on fail to connect.
	 */
	/*
	public boolean attachOutput(WirePin p_output){
		int i = 0;
		
		while (i <= this.outputs.size()){
			if (this.outputs.get(i) == null){
				i++;
			}
			else if (this.outputs.get(i) != null){
				this.outputs.get(i).setPartner(p_output);
				this.outputs.get(i).getPartner().setPartner(this.outputs.get(i));
				return true;
			}
		}
		
		return false;
	}
	*/
	
	
	/**
	 * Sets the 'name' of the Chip.  A normal chip doesn't have a unique name
	 * It is named after the type of chip it has: i.e. every AND gate is named
	 * "And".  Once the name has been set, an exception is thrown if you try to
	 * change it.  If you want to change the chip's name, make a new one.
	 * @param chipName (String) is the desired name.
	 * 
	 * @throws IllegalStateException if the chip has already had it's name set
	 */
	public void setName(String p_name){
		if (!((p_name == "") || (p_name == null)))
			{
				this.name = p_name;
			
		}
//		else
//		{
//			throw new IllegalStateException("BAD: Chips can not change their name once already set");
//		}
	}
	
	
	/**
	 * Returns the 'name' of the Chip.  For normal chips, this is actually the type of the chip,
	 * rather than any sort of unique identifier
	 * @return the 'name' of the Chip.
	 */
	public String getName(){
		return this.name;
	}
	
	
	/*
	 * dead code
	/**
	 * Checks to see if the parameter 'theirPin' (Pin) is connected to any
	 * inputs or outputs of this chip. 
	 * @param 'theirPin' is a Pin to check for connectedness with this chip.
	 * @return a boolean with value true if 'theirPin' is connected to the chip,
	 * else false.
	 */
	/*@Override
	public boolean isConnected(Pin theirPin) {
		for (int i = 0; i < this.inputs.size(); i++){
			if (this.inputs.get(i) == theirPin.getPartner()) return true;
		}
		
		for (int i = 0; i < this.outputs.size(); i++){
			if (this.outputs.get(i) == theirPin.getPartner()) return true;
		}
		
		return false;
	}*/
	
	/*
	 * dead code, not used anymore
	/**
	 * Attaches ourPin to theirPin.
	 * @param ourPin, theirPin (Pin). These are pins to connect to each other.
	 * @return a boolean with value true if ourPin successfully connects
	 * to theirPin, else false.
	 */
	/*private boolean attachTo(Pin ourPin, Pin theirPin) {
		if (ourPin == null)
		{
			return false;
		}
		
		if (ourPin.getParent() != this)
		{
			return false;
		}
		
		if (theirPin == null){
			return false;
		}
		
		if (theirPin != null)
		{
			// theirPin was already attached to something,
			// it needs to be detached first.
			if (theirPin.getPartner() != null){
				return false;
			}
		}
		
		if (theirPin instanceof ChipPin){
			return false;
		}
		
		if (theirPin.getParent() == this)
		{
			throw new IllegalStateException("BAD: Chip.attachTo(Pin,Pin): A pin which is not owned by us is saying that it is owned by us");
		}
		
		if (ourPin.getPartner() != null)
		{
			//We were already attached to something, reject the connection
			return false;
		}
		
		ourPin.setPartner(theirPin);
		theirPin.setPartner(ourPin);
		
		return true;
	}
	*/
	
	
	/**
	 * Finds out if testPin is in the inputs of Chip.
	 * @param testPin (Pin), is the Pin to be tested.
	 * @return -1 if no result found, else index at which it can be found.
	 */
	public boolean isInputPin(Pin testPin){
		if (testPin == null){
			return false;
		}
		
		for (int i = 0; i < this.inputs.size(); i++){
			if (this.inputs.get(i) == testPin) return true;
		}
		
		return false;
	}
	
	
	/**
	 * Finds out if testPin is in the outputs of Chip.
	 * @param testPin (Pin), is the Pin to be tested.
	 * @return -1 if no result found, else index at which it can be found.
	 */
	public boolean isOutputPin(Pin testPin){
		if (testPin == null){
			return false;
		}
		
		for (int i = 0; i < this.outputs.size(); i++){
			if (this.outputs.get(i) == testPin) return true;
		}
		
		return false;
	}
	
	
	/**
	 * Detaches ourPin to theirPin.
	 * @param ourPin, theirPin (Pin). These are pins to disconnect from
	 * one another.
	 * @return a boolean with value true if ourPin successfully disconnects
	 * from theirPin, else false.
	 */
	private boolean detachFrom(Pin ourPin, Pin theirPin) {
		if (ourPin == null)
		{
			return false;
		}
		if (ourPin.getParent() != this)
		{
			return false;
		}
		if (theirPin == null)
		{
			return false;
		}
		if (theirPin.getPartner() != ourPin)
		{
			return false;
		}
		else if (ourPin.getPartner() != theirPin)
		{
			throw new IllegalStateException("BAD: In a two pin pair, only one of the pins was properly connected");
		}
		else 
		{
			theirPin.setPartner(null);
			ourPin.setPartner(null);
			return true;
		}
	}
	
	/**
	 * Helper function which just cuts down a little on code spammage
	 * Detaches the Chip from the given WirePin
	 * @param theirPin A WirePin which is currently attached to this Chip
	 * @return true if the operation succeeded, else false
	 */
	private boolean detachFrom(Pin theirPin)
	{
		if (theirPin == null)
		{
			//nothing to detach!
			return false;
		}
		
		return detachFrom(theirPin.getPartner(), theirPin);
		
	}
	
	/*
	 * dead code
	/**
	 * Selects the chip.
	 */
	/*@Override
	public void select() {
		this.selected = true;
	}*/
	
	/*
	 * dead code
	/**
	 * Deselects the chip.
	 */
	/*@Override
	public void deselect() {
		this.selected = false;
	}*/
	
	/*
	 * dead code
	/**
	 * @return the state of the boolean variable 'selected'.
	 */
	/*
	public boolean isSelected(){
		return this.selected;
	}*/
	
	
	/**
	 * @return a copy of this chip's rectangle, which describes the location and shape of the chip.
	 */
	public Rectangle getRectangle(){
		return new Rectangle(this.rect);
	}
	
	/**
	 * Returns a ChipPin which is close to the given point, null there
	 * is no near by ChipPin
	 * @param point
	 * @return A ChipPin which is close to the given point, null if there is no near by ChipPin
	 */
	public ChipPin getNearbyPin(Point point)
	{
		for (ChipPin input : inputs)
		{
			if (input.closeToPin(point))
			{
				return input;
			}
		}
		
		for (ChipPin output : outputs)
		{
			if (output.closeToPin(point))
			{
				return output;
			}
		}
		return null;
	}
	
	
	/**
	 * Is necessary for the Serializable implementation. Passes a serialized version of the object to the
	 * ObjectOutputStream passed in as a parameter.
	 * @param out, an ObjectOutputStream to which the serialized version of this object is passed.
	 */
	@Override
	public void writeObject(ObjectOutputStream out) throws IOException
	{
		out.defaultWriteObject(); 
	}
	
	/**
	 * Is necessary for the Serializable implementation. Creates an object from the
	 * ObjectInputStream passed in as a parameter.
	 * @param in, an ObjectInputStream from which to create the object.
	 */
	@Override
	public void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		// our "pseudo-constructor"
		in.defaultReadObject();
	
	}
}

	